package ca.polymtl.rubikcube.CubeActivity;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Random;
import java.util.StringTokenizer;
import java.util.Vector;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Toast;
import ca.polymtl.rubikcube.R;
import ca.polymtl.rubikcube.CubeActivity.CubeSurfaceViewRenderer.AnimationCompleteCallback;
import ca.polymtl.rubikcube.CubeActivity.model.Face;
import ca.polymtl.rubikcube.CubeActivity.model.MoveSpec;
import ca.polymtl.rubikcube.CubeActivity.model.Rubik;
import ca.polymtl.rubikcube.CubeActivity.model.Side;
import ca.polymtl.rubikcube.CubeActivity.model.Rubik.RotationDirection;
import ca.polymtl.rubikcube.util.Color;

public class CubeActivity extends Activity implements AnimationCompleteCallback {
	private CubeSurfaceView cubeSurfaceView;
	private final static String LOG_TAG = CubeActivity.class.getSimpleName();
	Random rnd = new Random();

	Rubik rubik = new Rubik();

	Vector<MoveSpec> currentSolution = null;
	int currentSolutionIndex = 0;

	private Button solveButton;
	private Button shuffleButton;
	private Button previousButton;
	private Button nextButton;
	private Button exitSolvingModeButton;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		Log.d(LOG_TAG, "onCreate");

		Window window = this.getWindow();
		window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

		setContentView(R.layout.cube_layout);

		cubeSurfaceView = (CubeSurfaceView) findViewById(R.id.cubeSurfaceView);
		cubeSurfaceView.setRubik(rubik);
		cubeSurfaceView.setAnimationCompleteCallback(this);

		solveButton = (Button) findViewById(R.id.solveButton);
		shuffleButton = (Button) findViewById(R.id.shuffleButton);
		previousButton = (Button) findViewById(R.id.previousButton);
		nextButton = (Button) findViewById(R.id.nextButton);
		exitSolvingModeButton = (Button) findViewById(R.id.exitSolvingModeButton);

		Intent startIntent = getIntent();
		int[] initialColors = startIntent.getIntArrayExtra("ca.polymtl.rubikcube.initialColors");

		if (initialColors != null) {
			Log.d(LOG_TAG, "Specified initial colors");
			rubik.assignColors(initialColors);
		} else {
			Log.d(LOG_TAG, "Did not specify initial colors");
		}
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		super.onCreateOptionsMenu(menu);

		Log.d(LOG_TAG, "CubeActivity onCreateOptionsMenu");

		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.menu, menu);

		return true;
	}

	@Override
	protected void onPause() {
		super.onPause();
		cubeSurfaceView.onPause();
		Log.d(LOG_TAG, "onPause");
	}

	@Override
	protected void onResume() {
		super.onResume();
		cubeSurfaceView.onResume();
		Log.d(LOG_TAG, "onResume");
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		Log.d(LOG_TAG, "onOptionsItemSelected called " + Integer.toHexString(item.getItemId()));
		Side side;
		switch (item.getItemId()) {
		case R.id.sideFront:
			side = Side.FRONT;
			break;
		case R.id.sideBack:
			side = Side.BACK;
			break;
		case R.id.sideUp:
			side = Side.UP;
			break;
		case R.id.sideDown:
			side = Side.DOWN;
			break;
		case R.id.sideRight:
			side = Side.RIGHT;
			break;
		case R.id.sideLeft:
			side = Side.LEFT;
			break;
		case R.id.sideMiddle_FB:
			side = Side.MIDDLE_FB;
			break;
		case R.id.sideMiddle_UD:
			side = Side.MIDDLE_UD;
			break;
		case R.id.sideMiddle_RL:
			side = Side.MIDDLE_RL;
			break;
		default:
			return super.onOptionsItemSelected(item);
		}

		if (!inSolvingMode()) {
			Log.d(LOG_TAG, "I'm asking for a rotation on " + side + " CW");
			this.rotateFace(side, RotationDirection.CW, true);
		} else {
			Toast.makeText(this, "You can't make an arbitrary move while in solving mode !", Toast.LENGTH_LONG).show();
		}
		return true;
	}

	public void shuffleButtonClicked(View v) {
		Side sides[] = Side.values();
		RotationDirection directions[] = RotationDirection.values();

		for (int i = 0; i < 100; i++) {
			Side side = sides[rnd.nextInt(6)];
			RotationDirection direction = directions[rnd.nextInt(2)];

			this.rotateFace(side, direction, false);
		}
	}

	private void rotateFace(Side side, RotationDirection direction, boolean animation) {
		finishRotateFace();

		if (animation) {
			runOnUiThread(new Runnable() {

				public void run() {
					disablePrevNextButtons();
				}
			});

			cubeSurfaceView.rotateFace(side, direction);
		} else {
			rubik.rotateFace(side, direction);
		}
	}

	private void finishRotateFace() {
		cubeSurfaceView.finishRotateFace();
	}

	private String generateFaceParam(String key, Color[][] colors) {
		String result = key + "=";

		for (int j = 0; j < 3; j++) {
			for (int i = 0; i < 3; i++) {
				result += colors[i][j].name.charAt(0);
			}
		}

		return result;
	}

	private String generateUrlParams() {
		String[] keys = { "f", "u", "r", "b", "d", "l" };
		String paramsString = new String();

		Vector<String> params = new Vector<String>();

		Side[] sides = Side.values();

		for (Side side : sides) {
			Face face = rubik.getFace(side);

			Color[][] colors = face.getColors();

			switch (side) {
			case RIGHT:
				colors = Face.rotateRight(colors);
				break;
			case BACK:
				colors = Face.rotateRight(colors);
				break;
			case DOWN:
				colors = Face.rotateLeft(colors);
				break;
			}

			String s = generateFaceParam(keys[side.ordinal()], colors);
			params.add(s);
		}

		for (int i = 0; i < params.size() - 1; i++) {
			paramsString += params.get(i) + "&";
		}

		paramsString += params.lastElement();

		return paramsString;
	}

	private BufferedReader fetchSolutionFromServer() throws ClientProtocolException, IOException, URISyntaxException {
		String params = generateUrlParams();
		Log.d(LOG_TAG, "Params: " + params);

		String host = "nova.polymtl.ca";
		String path = "/~simark/solverB/query.php";
		String url = "http://" + host + path + "?" + params;
		Log.d(LOG_TAG, "Complete URL: " + url);

		HttpClient client = new DefaultHttpClient();
		HttpGet request = new HttpGet();
		request.setURI(new URI(url));
		HttpResponse response = client.execute(request);
		InputStream responseContent = response.getEntity().getContent();

		InputStreamReader reader = new InputStreamReader(responseContent);

		return new BufferedReader(reader);
	}

	private Vector<MoveSpec> parseServerSolution(BufferedReader solutionBufferedReader) throws IOException,
			UnrecognizedMoveTokenException, UnrecognizedServerStatusException, ErrorStatusException {
		String status = solutionBufferedReader.readLine();
		String content = solutionBufferedReader.readLine();

		Log.d(LOG_TAG, "Status: " + status);
		Log.d(LOG_TAG, "Content: " + content);

		if (status.equals("OK")) {
			return convertSolutionInput(content);
		} else if (status.equals("ERROR")) {
			throw new ErrorStatusException(content);
		} else {
			throw new UnrecognizedServerStatusException(status);
		}
	}

	private void showSolvingModeButtons() {
		solveButton.setVisibility(View.GONE);
		shuffleButton.setVisibility(View.GONE);

		previousButton.setVisibility(View.VISIBLE);
		nextButton.setVisibility(View.VISIBLE);
		exitSolvingModeButton.setVisibility(View.VISIBLE);
	}

	private void showNormalModeButtons() {
		solveButton.setVisibility(View.VISIBLE);
		shuffleButton.setVisibility(View.VISIBLE);

		previousButton.setVisibility(View.GONE);
		nextButton.setVisibility(View.GONE);
		exitSolvingModeButton.setVisibility(View.GONE);
	}

	private void enterSolvingMode() {
		if (inSolvingMode()) {
			Log.w(LOG_TAG, "Already in solving mode !");
			return;
		}

		try {
			BufferedReader solutionBufferedReader = fetchSolutionFromServer();
			currentSolution = parseServerSolution(solutionBufferedReader);

			if (currentSolution.size() > 0) {
				currentSolutionIndex = 0;
				showSolvingModeButtons();
				Toast.makeText(this, "Solution loaded, it has " + currentSolution.size() + " moves.", Toast.LENGTH_LONG)
						.show();
			} else {
				Toast.makeText(this, "The cube is already solved", Toast.LENGTH_LONG).show();
				currentSolution = null;
			}

		} catch (URISyntaxException e) {
			Toast.makeText(this, "Exception : " + e.toString(), Toast.LENGTH_LONG).show();
		} catch (ClientProtocolException e) {
			Toast.makeText(this, "Exception : " + e.toString(), Toast.LENGTH_LONG).show();
		} catch (IOException e) {
			Toast.makeText(this, "Exception : " + e.toString(), Toast.LENGTH_LONG).show();
		} catch (UnrecognizedMoveTokenException e) {
			Toast.makeText(this, "Exception : " + e.toString(), Toast.LENGTH_LONG).show();
		} catch (UnrecognizedServerStatusException e) {
			Toast.makeText(this, "Exception: did not understand response from server (" + e.toString() + ")",
					Toast.LENGTH_LONG).show();
		} catch (ErrorStatusException e) {
			Toast.makeText(this, "Error: " + e.toString(), Toast.LENGTH_LONG).show();
		}

	}

	public void solveButtonClicked(View v) {
		enterSolvingMode();
	}

	private boolean validateMoveToken(String token) {
		boolean firstCharValid = false, secondCharValid = false;
		char char0 = token.charAt(0);
		char char1 = token.charAt(1);

		for (Side s : Side.values()) {
			if (char0 == s.name().charAt(0)) {
				firstCharValid = true;
				break;
			}
		}

		switch (char1) {
		case '2':
		case '\'':
		case ' ':
			secondCharValid = true;
			break;
		}

		return firstCharValid && secondCharValid;
	}

	private Vector<MoveSpec> convertSolutionInput(String solutionString) throws UnrecognizedMoveTokenException {
		Vector<MoveSpec> moves = new Vector<MoveSpec>();
		StringTokenizer tokenizer = new StringTokenizer(solutionString);

		while (tokenizer.hasMoreTokens()) {
			String token = tokenizer.nextToken();
			Side side = null;
			MoveSpec move;

			RotationDirection direction = RotationDirection.CW;

			/* Make sure they are all of length 2 */
			if (token.length() < 2) {
				token += ' ';
			}

			if (!validateMoveToken(token)) {
				throw new UnrecognizedMoveTokenException("unrecognized move token (" + token + ")");
			}

			/* Find the side by trying to match the first letter of each side name */
			for (Side s : Side.values()) {
				if (token.charAt(0) == s.name().charAt(0)) {
					side = s;
					break;
				}
			}

			/* Invert the direction if side is followed by ' */
			if (token.charAt(1) == '\'') {
				direction = RotationDirection.CCW;
			}

			/* Add the move */
			move = new MoveSpec(side, direction);
			moves.add(move);

			/* Replace 180 degrees move by two successive moves of the same face */
			if (token.charAt(1) == '2') {
				moves.add(move);
			}
		}

		return moves;
	}

	private void exitSolvingMode() {
		if (!inSolvingMode()) {
			Log.w(LOG_TAG, "exitSolvingMode: already not in solving mode !");
			return;
		}

		showNormalModeButtons();
		currentSolution = null;
	}

	private void undoPreviousSolutionMove() {
		if (!inSolvingMode()) {
			Log.w(LOG_TAG, "doNextSolutionMove called while not in solving mode");
			return;
		}

		if (!hasPreviousMove()) {
			Toast.makeText(this, "No more previous moves !", Toast.LENGTH_LONG).show();
			return;
		}

		currentSolutionIndex--;
		MoveSpec currentMove = currentSolution.get(currentSolutionIndex);
		currentMove = currentMove.oppositeMove();

		Log.d(LOG_TAG, "Turning " + currentMove.side + " " + currentMove.direction);

		this.rotateFace(currentMove.side, currentMove.direction, true);
	}

	private void doNextSolutionMove() {
		if (!inSolvingMode()) {
			Log.w(LOG_TAG, "doNextSolutionMove called while not in solving mode");
			return;
		}

		if (!hasNextMove()) {
			showToastOnMainThread("No more next moves !");
			return;
		}

		MoveSpec currentMove = currentSolution.get(currentSolutionIndex);
		currentSolutionIndex++;

		Log.d(LOG_TAG, "Turning " + currentMove.side + " " + currentMove.direction);

		this.rotateFace(currentMove.side, currentMove.direction, true);
	}

	boolean inSolvingMode() {
		return currentSolution != null;
	}

	boolean hasNextMove() {
		if (inSolvingMode()) {
			return currentSolutionIndex < currentSolution.size();
		} else {
			return false;
		}
	}

	boolean hasPreviousMove() {
		if (inSolvingMode()) {
			return currentSolutionIndex > 0;
		} else {
			return false;
		}
	}

	public void animationCompleted() {
		Log.d(LOG_TAG, "animationCompleted");

		if (!hasNextMove()) {
			showToastOnMainThread("And voilà !");
		}

		runOnUiThread(new Runnable() {
			public void run() {
				enablePrevNextButtons();
			}
		});
	}

	public void previousButtonClicked(View v) {
		undoPreviousSolutionMove();
	}

	public void nextButtonClicked(View v) {
		doNextSolutionMove();
	}

	public void exitSolvingModeButtonClicked(View v) {
		exitSolvingMode();
	}

	/* Used to show a toast from any thread */
	public void showToastOnMainThread(String text) {
		Message msg = toastHandler.obtainMessage();

		Bundle data = new Bundle();
		data.putString("toastText", text);

		msg.setData(data);

		toastHandler.sendMessage(msg);
	}

	private Handler toastHandler = new Handler() {
		public void handleMessage(android.os.Message msg) {
			String text = msg.getData().getString("toastText");

			if (text != null) {
				showToast(text);
			}
		};
	};

	private void enablePrevNextButtons() {
		previousButton.setEnabled(true);
		nextButton.setEnabled(true);
		exitSolvingModeButton.setEnabled(true);
	}

	private void disablePrevNextButtons() {
		previousButton.setEnabled(false);
		nextButton.setEnabled(false);
		exitSolvingModeButton.setEnabled(false);
	}

	private void showToast(String text) {
		Toast.makeText(this, text, Toast.LENGTH_LONG).show();
	}
}
